docker编排+低代码部署Headscale VPN打通大内网

docker编排+低代码部署Headscale VPN打通大内网

3 人赞同了该文章
目录
收起
为什么是VPN?
用哪个VPN?
为什么是Headscale?
本文动机
Headscale搭建
架构介绍
容器编排
服务端配置
反向代理配置
服务端操作(步骤1)
UI端操作(步骤2)
客户端操作(步骤3)
NAS配置(步骤4)
主路由配置(步骤5)
贡献

为什么是VPN?

前面讲过,我企图打通各个住所和学校的内网。列位要问了,你不是搞过FRP内网穿透吗,为啥还要VPN?我个人的理解是:FRP侧重于服务,依托于开放的端口;VPN侧重于互连,依托于C/S架构和IP,对比于下表。可见,要打通各个内网,必须使用基于VPN的技术才行。

对比项FRPVPN
开放端口数随服务数增加很少
主要应用对外提供服务,网页服务较多对内提供连通
穿透方向单向双向(通过路由)
安全性一般
IP级互连不支持支持
额外的客户端不需要一般需要
部署难度容易困难

用哪个VPN?

关于主流VPN技术,下面这篇文章总结的挺好。

我斗胆再一句话总结下:PPTP不安全;OpenVPN针对IPSec/L2TP做了减法;WireGuard针对OpenVPN又做了减法,性能更高,还支持了去中心化。

可见,WireGuard是目前最先进的VPN技术,已被引入Linux内核,必须选她!

还有个原因,群晖的VPN服务端都被阉割了,自己装套件起OpenVPN也不行;威联通的OpenVPN服务端可以,但静态路由设置时总是出错。

为什么是Headscale

WireGuard目前只是一个内核级别的模块,想要配置好裸的WireGuard,低代码是别想了,那么多对端秘钥,增、删节点都需要改动所有节点的配置,想一想就头疼!

表扬威联通,已经支持图形化界面的WireGuard服务器和客户端。

基于WireGuard的上层应用,目前比较成熟的有TailscaleNetmakerTailscale 是在用户态实现了 WireGuard 协议;Netmaker 直接使用了内核态的WireGuard,理论上性能更高,但目前缺乏中继机制(类似FRP),应用场景受限。HeadscaleTailscale的开源实现,适合私有部署,就选她了!

本文动机

知乎上介绍Headscale的很少;找遍全网,也很少有低代码、快速部署Headscale的文章,能讲清楚原理和为什么这样配置的就更少了。

仍然要感谢一些博主,虽然不讲原理,但内容确实丰富,给我一定启发(其实是偷懒不用去看文档了),比如下面这个。

我在群晖和威联通的NAS上都用docker-compose部署成功了,必须向大家汇报下,希望能帮助更多非专业领域的“私有云折腾师”。

Headscale搭建

架构介绍

主节点(我自己定义的概念)的网络拓扑如下图所示。其他节点与之类似,不包含服务端及其UI。

主节点网络拓扑

服务端(server),又叫协调服务器。负责WireGuard节点的公钥交换、虚拟IP分配、路由转发的公开和访问控制。

客户端(client),即WireGuard节点。目前仍然使用的是Tailscale的开源客户端,采用go语言编写,在用户空间实现WireGuard

中继端(derp),是P2P连接时NAT穿透的保底方案。DERP(Detoured Encrypted Routing Protocol)是Tailscale自研的协议,运行在 HTTP 之上 ,根据目的公钥来中继加密的流量。中继端同时支持DERP和STUN。

关于NAT穿透的原理,可以参考下面这篇。

可见,服务端负责控制,中继端负责数据通路,客户端发起/接受连接,是可以部署在不同的服务器上的。这里我们资源有限,把他们都部署在一个NAS里,还需要使用反向代理(lucky以“零代码”支持带SSL证书的HTTPS访问;为了“低代码”配置服务端,我们给她再加一个服务端控制界面(webui,以下简称UI端),齐活。

关于客户端,其实有两个作用。一是做为WireGuard节点连到大内网里。

这时,为了减少路由的层级,其容器的网络类型一般设为host。

二是通过Unix的进程间通信(sock)为中继端提供用户认证,防止中继端被他人使用。

通过把客户端和中继端的/var/run/headscale链接在一起来实现。这时,其容器的网络类型最好设为bridge。

如何选择容器网络类型,可以参考下面的公式。

假设,中继端部署在服务器A上,负责VPN路由的是服务器B。
if(A == B)
  在A上部署客户端,容器网络使用host。
else {
  在A上部署客户端,容器网络使用bridge或host都行。
  在B上部署客户端;如果使用容器,其网络使用host。// 例如,OpenWRT上可以直接部署。
}
关于自定义的容器子网,可以参考下面这篇文章。

我把这些容器都部署在一个NAS上,所以用host。相关的端口如下表,使用了基于子域名的lucky反向代理后,只需要对公网(别忘了在路由器上做端口映射)暴露一个STUN的UDP3478端口(新增)和一个lucky反向代理的端口(例如8080,已有)。相比FRP,美极了。

服务端UI端中继端DERP中继端STUN
端口类型TCPTCPTCPUDP
容器侧端口8080707060603478
NAS侧端口5808057070560603478
HTTPS反向代理需要需要需要不需要

容器编排

直接给出带注释的四合一docker-compose.yaml,全网罕见。

version: '3.9'

networks: # 定义编排容器的子网
  private:
    driver: bridge
    ipam:
      config:
        - subnet: 172.18.200.0/24

services:
  server: # 服务端
    image: headscale/headscale
    container_name: headscale-server
    networks:
      - private
    volumes:
      - ./headscale/config:/etc/headscale # 提前放好config.yaml和derp.yaml
      - ./headscale/data:/var/lib/headscale
      - ./headscale/run:/var/run/headscale
      - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro # 使用NAS的时间
    ports:
      - "58080:8080" # listen port
    command: serve # v0.22及以前的版本需要使用headscale serve
    restart: unless-stopped
    depends_on:
      - derp

  webui: # UI端
    image: ghcr.io/gurucomputing/headscale-ui
    container_name: headscale-ui
    networks:
      - private
    environment:
      HTTP_PORT: 7070
    ports:
      - "57070:7070" 
    volumes:
      - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
    restart: unless-stopped

  derp: # 中继端
    image: fredliang/derper
    container_name: headscale-derp
    networks:
      - private
    environment:
      DERP_DOMAIN: derp.example.com # 替换为自己的域名
      DERP_ADDR: :6060 # 注意,前面有个英文冒号
      DERP_CERT_MODE: letsencrypt # 使用了lucky做反向代理,理论上不需要设置,但我还没试过。
      DERP_VERIFY_CLIENTS: true # 还用client做认证时,配置为true
    ports:
      - "56060:6060" # derp port, TCP
      - "3478:3478/udp"  # STUN port, UDP
    volumes:
      - ./tailscale:/var/run/tailscale
      - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
    restart: unless-stopped
    depends_on:
      - client

  client: # 客户端
    image: tailscale/tailscale
    container_name: headscale-client
    network_mode: "host" # 用做连接各子网的客户端时,这样最简单
    privileged: true
    environment:
      TS_EXTRA_ARGS: --netfilter-mode = off # 默认不开启路由转发,更灵活
    volumes:
      - ./tailscale:/var/run/tailscale # 要在NAS上和derp共享同一个目录
      - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
      - /var/lib:/var/lib
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - net_admin
      - sys_module
    command: tailscaled
    restart: unless-stopped

注意,要提前配置好config.yamlderp.yaml。可以去GitHUB的代码仓,下载config-example.yamlderp-example.yaml,修改好内容(见下文)并重命名。

我用的是latest映像,当前对应源码的版本是v0.23.0-alpha5。配置文件如果报错,可以去搜一下Issues,一般都有答案。

另外,只需要把docker-compose.yamlserverwebui的部分注释掉,就可以部署在其他节点。如果不想增加中继端,也可以把derp的部分注释掉。

服务端配置

config.yaml中修改的地方如下。

  • server_url要改成反向代理后的网址。
  • urls下面的网址注释掉,不使用官方的中继端。
  • 增加derp.yaml的位置,指定自己搭建的中继端。
  • 注意各端口要和docker-compose.yaml中的对应。
server_url: https://headscale.example.com:8080
listen_addr: 0.0.0.0:8080
# Address to listen to /metrics, you may want to keep this endpoint private to your internal network
metrics_listen_addr: 0.0.0.0:9090
grpc_listen_addr: 0.0.0.0:50443 # 看起来没啥用
ip_prefixes:
  100.100.0.0/16
  # List of externally available DERP maps encoded in JSON
  urls:
    #- https://controlplane.tailscale.com/derpmap/default

  # Locally available DERP map files encoded in YAML
  paths:
    - /etc/headscale/derp.yaml

derp.yaml如下,这里我添加了两个中继端。

regions:
  900:
    regionid: 900
    regioncode: hz
    regionname: China Telecom Hangzhou
    nodes:
      - name: shelter1-derp
        regionid: 900
        hostname: derp.example.com
        stunport: 3478
        stunonly: false
        derpport: 8080
      - name: shelter2-derp
        regionid: 900
        hostname: derp.mirror.example.com
        stunport: 3478
        stunonly: false
        derpport: 8080

反向代理配置

服务端和中继端的反向代理没什么可说的,唯有UI端有点特殊。

headscale-ui这个项目原始的计划是和服务端部署在同一个服务器里,例如:服务端通过协议://域名:端口号访问,UI端就必须通过协议://域名:端口号/web访问。其原理是UI端调用服务端的HTTP的API(需要配置API Key)来操作服务端,这就带来一个问题:使用容器部署时,不能随意设置反向代理,否则会有跨域访问被拒绝的问题。

必须保证服务端和UI端使用相同的域名相同的端口号相同的协议(HTTPS/HTTP)。因此,很多教程去介绍如何用NginxCaddyTraefik等反向代理工具的配置,需要写大量代码,要弄懂得学习HTTP传输原理。我在lucky里探索出一个非常简单的方法,零代码解决,必须分享给大家。

关于lucky反向代理,可以参考下面这篇文章。

首先,服务端和UI端使用相同的代理端口,例如8080。如果服务端的“前端域名/地址”是headscale.example.com,UI端的“前端域名/地址”就填headscale.example.com/web,UI端的“后端地址”就填NAS的IP地址:57070(UI的NAS侧端口号)/web。这样,访问https://headscale.example.com:8080/web就能访问到UI端且后续操作不会产生跨域问题。

这时,服务端的URL是https://headscale.example.com:8080

服务端操作(步骤1)

其实服务端支持很多命令行操作,但我们追求“低代码”,只需要用命令行生成一个API Key,剩下的工作在UI端点鼠标就行了。

进入容器,执行命令,把生成的API Key记录下来

$ headscale apikeys create -e 9999d

其中,-e后面指定的是过期时间,这里我指定9999天,27年后看能否有人攻破。

也可以在宿主机上执行,前面加sudo docker exec -it即可,不会的可以练练。

UI端操作(步骤2)

  1. 打开UI的URL,本例为https://headscale.example.com:8080/web

2. 进入“Settings”。

3. 添加“Headscale URL”,本例为https://headscale.example.com:8080

4. 把服务端生成的Key添加到“Headscale API Key”。

5. 点击“Test Server Settings”,出现绿色对号后UI端就可以接管服务端了,如下图所示。

UI端添加API Key

6. 进入“User View”,点击“+New User”,添加一个用户。

UI端添加用户

7. 为该用户生成一个Preauth Key,供客户端连接使用。为了便捷性,最好设置为“Reusable”,并“Active”,如下图。

UI端添加Preauth Key
连接的密钥设置比较灵活,有两种方法。一种是上面这种:在服务端生成Preauth Key(1个共享或多个独立),客户端连接时指定,成功后在“Device View”里就能看到各个节点。另一种是在客户端连接时生成,在UI端的“Device View”里手动添加秘钥、注册节点。我这么懒惰,当然共享1个Preauth Key。

客户端操作(步骤3)

  1. 进入各客户端的容器,执行命令。
$ tailscale up --netfilter-mode=off \
               --accept-routes \
               --advertise-routes=192.168.3.0/24 \
               --login-server=https://headscale.example.com:8080 \
               --auth-key=xxxxxxxxxxxx
  • --accept-routes代表接受其他节点的路由指示。
  • --advertise-routes指定本节点对其他节点的路由建议,即哪个网段走VPN到本节点。一般是本节点的内网网段。
  • --login-server指定服务端的URL。
  • --auth-key指定在UI端生成的Preauth Key。

2. 打开UI端网页,进入“Device View”,把各节点的“Device Routes”设置为“active”,如下图。

这里还可以看到各个节点分配的VPN IP地址。
UI端开启Devic Routes

NAS配置(步骤4)

要在NAS上开启路由转发,把VPN路由过来的包转发到内网。

  1. 通过ssh登录到NAS,执行命令。
$ ip addr

2. 找到NAS的内网IP地址所对应的虚拟网卡名,我这里是ovs_eth0;找到VPN地址所对应的网卡名,我这里是tailscale0

3. 执行命令:启用IPv4转发功能;防火墙配置了两个网络接口(ovs_eth0tailscale0)的数据包转发规则,并执行网络地址转换(NAT)操作。使能了VPN子网和内网的双向互访。

$ sudo iptables -I FORWARD -i ovs_eth0 -j ACCEPT
$ sudo iptables -I FORWARD -o ovs_eth0 -j ACCEPT
$ sudo iptables -t nat -I POSTROUTING -o ovs_eth0 -j MASQUERADE
$ sudo iptables -I FORWARD -i tailscale0 -j ACCEPT
$ sudo iptables -I FORWARD -o tailscale0 -j ACCEPT
$ sudo iptables -t nat -I POSTROUTING -o tailscale0 -j MASQUERADE
$ sudo sysctl -w net.ipv4.ip_forward=1

4. 最后,把它们加到群晖的“计划任务”,开机触发启动。

  • 去掉所有sudo,以root执行。
  • 为了保证VPN相关的容器先启动,最上面最好加个sleep 1m

主路由配置(步骤5)

为了让本节点内网的其他地址也能通过VPN访问其他节点的内网,需要在主路由上添加静态路由,例如下表。

描述目的地址子网掩码下一跳地址出接口
访问VPN节点100.100.0.0255.255.0.0本节点NAS地址LAN
访问其他节点的内网其他节点的内网网段其他节点的内网掩码本节点NAS地址LAN

经过ping测试,大功告成!

贡献

本文介绍了docker-compose配合lucky反向代理实现Headscale VPN快速私有化部署的方法、流程和大致原理。绝大多数操作都在UI网页端,将“低代码”进行到底。

免费的SD-WAN,唾手可得。

编辑于 2024-04-11 22:13・IP 属地浙江
「真诚赞赏,手留余香」
还没有人赞赏,快来当第一个赞赏的人吧!
x1
>
<
>>
<<
O
x1

Powered by Yiting & Majiang